/*
 * SIT_ComboBox.c : drop down list with user editable text area.
 *
 * Written by T.Pierron, Dec 7, 2008.
 */

#include "SIT_P.h"
#include <stdio.h>
#include <ctype.h>

	TagList ComboClass[] = {
		{ SIT_SuperClass,    NULL,            ___, SIT_INT,  (ULONG) WidgetClass },
		{ SIT_InitialValues, "initialValues", CSG, SIT_PTR,  OFFSET(SIT_ComboBox, sc_InitValues) },
		{ SIT_SelectedIndex, "selectedIndex", _SG, SIT_INT,  OFFSET(SIT_ComboBox, sc_SelIndex) },
		{ SIT_ReadOnly,      "readOnly",      _SG, SIT_BOOL, OFFSET(SIT_ComboBox, sc_ReadOnly) },
		{ SIT_AutoComplete,  "autoComplete",  C__, SIT_BOOL, OFFSET(SIT_ComboBox, sc_AutoComplete) },
		{ SIT_MinItemHeight, "minItemHeight", C__, SIT_INT,  OFFSET(SIT_ComboBox, sc_MinHeight) },
		{ SIT_ItemCount,     "itemCount",     __G, SIT_INT,  OFFSET(SIT_ComboBox, sc_Items) },
		{ SIT_TagEnd }
	};

static int SIT_FinalizeCombo(SIT_Widget, APTR cd, APTR ud);

/* Optimal width of combo box : check that all strings within dropdown list can be visible */
static int SIT_MeasureComboBox(SIT_Widget w, APTR cd, APTR ud)
{
	HDC      hdc = GetDC(w->sw_Handle);
	HGDIOBJ  old = SelectObject(hdc, w->sw_Font);
	LPWSTR   buf = NULL;
	SIZE *   ret = cd;
	int      max = 0;
	int      nb  = SendMessage(w->sw_Handle, CB_GETCOUNT, 0, 0);
	int      i;

	/* Compute maximal length (in chars) of string in the dropdown list */
	for (i = 0; i < nb; i ++)
	{
		int len = SendMessage(w->sw_Handle, CB_GETLBTEXTLEN, i, 0) + 1;

		if (max < len) max = len;
	}

	if (max > 0)
	{
		buf = alloca(max * sizeof *buf);

		for (i = max = 0; i < nb; i ++)
		{
			SendMessage(w->sw_Handle, CB_GETLBTEXT, i, (LPARAM) buf);
			GetTextExtentPoint32(hdc, buf, wcslen(buf), ret);
			if (max < ret->cx)
				max = ret->cx;
		}
	}
	else GetTextExtentPoint32(hdc, L"X", 1, ret), max = ret->cx;

	((SIT_ComboBox)w)->sc_ItemHeight = ret->cy;
	ret->cx  = max + 8 + GetSystemMetrics(SM_CYHSCROLL);
	ret->cy += 8;

	max = ret->cy-2;
	max = MAX(((SIT_ComboBox)w)->sc_MinHeight, max);

	SendMessage(w->sw_Handle, CB_SETITEMHEIGHT, -1, max);
	SendMessage(w->sw_Handle, CB_SETITEMHEIGHT,  0, max);

	SelectObject(hdc, old);
	ReleaseDC(w->sw_Handle, hdc);
	return 0;
}

void SIT_SetComboListWithTags(SIT_ComboBox cb)
{
	SIT_CBRow row;
	LPWSTR    buf = NULL;
	int       max, maxlen, len, i;

	for (max = maxlen = i = 0, row = cb->sc_RowTags; i < cb->sc_Items; i ++, row ++)
	{
		len = (MultiByteToWideChar(CP_UTF8, 0, row->entry, -1, NULL, 0) + 1) * sizeof (wchar_t);
		max += len;
		if (maxlen < len) maxlen = len;
	}

	/* Removes old entries */
	SendMessage(cb->super.sw_Handle, CB_RESETCONTENT, 0, 0);
	SendMessage(cb->super.sw_Handle, CB_INITSTORAGE,  cb->sc_Items, max);

	buf = alloca(maxlen * sizeof *buf);

	/* Add new */
	for (i = 0, row = cb->sc_RowTags; i < cb->sc_Items; i ++, row ++)
	{
		len = MultiByteToWideChar(CP_UTF8, 0, row->entry, -1, buf, maxlen);
		SendMessage(cb->super.sw_Handle, CB_ADDSTRING,   0, (LPARAM) buf);
		SendMessage(cb->super.sw_Handle, CB_SETITEMDATA, i, (LPARAM) row->tag);
	}
	SendMessage(cb->super.sw_Handle, CB_SETCURSEL, MIN(cb->sc_SelIndex, 0), 0);
	cb->super.sw_Flags &= ~SITF_Internal;
}

/* Change content of combo box */
static void SIT_SetComboList(SIT_ComboBox cb)
{
	STRPTR p, buf, next;
	int    i, max;

	SIT_FinalizeCombo(&cb->super, NULL, (APTR) 1);
	cb->sc_Values = NULL;
	cb->sc_RowTags = NULL;
	cb->sc_Count = cb->sc_Items = cb->sc_SelIndex = 0;
	cb->super.sw_Flags |= SITF_Internal; // Avoid posting event

	for (max = 0, next = p = cb->sc_InitValues; *p; p = next)
	{
		for (next = p; *next && *next != '\t'; next ++);
		i = next - p + 1;
		if (max < i) max = i;
		if (*next) next ++;
	}

	buf = alloca(max);

	/* Add new */
	for (p = cb->sc_InitValues; *p; p = next)
	{
		for (next = p; *next && *next != '\t'; next ++);
		CopyString(buf, p, next - p + 1);
		if (*next) next ++;
		SIT_ComboInsertItem(&cb->super, -1, buf, NULL);
	}
	SIT_SetComboListWithTags(cb);
}

static int SIT_SetComboStyles(SIT_Widget w, APTR cd, APTR ud)
{
	SIT_ComboBox cb = (SIT_ComboBox) w;

	if (w->sw_Flags & SITF_Style1Changed) /* Combo list changed */
		SIT_SetComboList(cb);

	if (w->sw_Flags & SITF_Style2Changed)
		SendMessage(w->sw_Handle, CB_SETCURSEL, cb->sc_SelIndex, 0);

	w->sw_PostProcess = NULL;
	cb->sc_Flags &= ~(SITF_Style1Changed | SITF_Style2Changed);
	return 0;
}

/* Handle setting values to custom attributes of ComboBox */
static int SIT_SetComboValues(SIT_Widget w, APTR cd, APTR ud)
{
	TagList *     tag   = cd;
	SIT_Variant * value = ud;
	SIT_ComboBox  cb    = (SIT_ComboBox) w;

	switch (tag->tl_TagID) {
	case SIT_InitialValues:
		cb->sc_InitValues = value->string; // XXX Maybe better to not keep that value
		w->sw_Flags |= SITF_Style1Changed;
		break;
	case SIT_SelectedIndex:
		cb->sc_SelIndex = value->integer;
		w->sw_Flags |= SITF_Style2Changed;
		SendMessage(w->sw_Handle, CB_SETCURSEL, value->integer, 0);
		break;
	case SIT_ReadOnly: /* Create only */
		cb->sc_ReadOnly = value->boolean;
		break;
	default:
		return SIT_SetWidgetValue(w, cd, ud); // super class
	}
	if (w->sw_Flags & SITF_StylesChanged) w->sw_PostProcess = SIT_SetComboStyles;
	return 0;
}

int SIT_ComboBoxItemHeight(SIT_Widget w)
{
	int nb  = SendMessage(w->sw_Handle, CB_GETCOUNT, 0, 0);
	int max = GetSystemMetrics(SM_CYSCREEN) - 40;

	if (nb > 15) nb = 15;
	if (nb <= 0) nb = 1;

	nb = nb * ((SIT_ComboBox)w)->sc_ItemHeight + 6;

	return MIN(nb, max);
}

static Bool DoAutoComplete(HWND combo, HWND edit, TCHAR ch)
{
	ULONG  max    = GetWindowTextLength(combo)+2;
	LPWSTR buffer = alloca(max * sizeof *buffer);
	LPWSTR item   = alloca((max + 100) * sizeof *item);
	LPWSTR temp   = NULL;
	int    i, nb, nth;

	GetWindowText(combo, buffer, max);

	// Handle keyboard input
	if (VK_RETURN == ch)
	{
		SendMessage(combo, CB_SHOWDROPDOWN, FALSE, 0);
		SendMessage(edit, EM_SETSEL, 0, -1); // Selects the entire item
		i = SendMessage(combo, CB_FINDSTRINGEXACT, -1, (LPARAM) buffer);
		SendMessage(combo, CB_SETCURSEL, i, 0);
		// Doesn't seem to send any notification code
		SIT_SubClassHandler(GetParent(combo), WM_COMMAND, (CBN_SELCHANGE<<16)|i, (LPARAM) combo);
		return True;
	}
	else if (iscntrl(ch)) return False;

	if (! SendMessage(combo, CB_GETDROPPEDSTATE, 0, 0))
	{
		SendMessage(combo, CB_SHOWDROPDOWN, TRUE, 0);
		SetFocus(edit);
	}

	// Get the substring from 0 to start of selection
	nb = SendMessage(combo, CB_GETCOUNT, 0, 0);
	nth = LOWORD(SendMessage(combo, CB_GETEDITSEL, 0, 0));
	buffer[nth] = 0;
	item[0] = ch; item[1] = 0; wcscat(buffer, item); nth ++;

	// ComboBoxEx doesn't have a substring match (regular ComboBox does) :-/
	for (i = 0, max += 100; i < nb; i ++)
	{
		int    len = SendMessage(combo, CB_GETLBTEXTLEN, i, 0) + 1;
		LPWSTR buf = len > max ? temp = realloc(temp, len) : item;
		SendMessage(combo, CB_GETLBTEXT, i, (LPARAM) buf);
		if (wcsnicmp(buf, buffer, nth) == 0) break;
	}
	if (temp) free(temp);

	SendMessage(combo, CB_SETCURSEL, i, 0);
	SendMessage(edit, EM_SETSEL, nth, -1);
	return True;
}

#define	cb   ((SIT_ComboBox)w)
static LRESULT CALLBACK SIT_ComboEditProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	SIT_Widget w = (APTR) GetProp(hwnd, sit_class);

	switch (msg) {
	case WM_KEYUP: if (VK_RETURN != wParam) break;
	case WM_CHAR:
		if (DoAutoComplete(w->sw_Handle, cb->sc_Edit, wParam))
			return 0;
	}
 	return CallWindowProc(cb->sc_EditProc, hwnd, msg, wParam, lParam);
}

static int SIT_FinalizeCombo(SIT_Widget w, APTR cd, APTR ud)
{
	if (cb->sc_EditProc && ud == NULL)
	{
		SetWindowLong(cb->sc_Edit, GWL_WNDPROC, (LONG) cb->sc_EditProc);
		RemoveProp(cb->sc_Edit, sit_class);
	}

	if (cb->sc_RowTags) free(cb->sc_RowTags);
	if (cb->sc_Values)  free(cb->sc_Values);

	return 0;
}

DLLIMP APTR SIT_ComboGetRowTag(SIT_Widget w, int i, STRPTR * msg)
{
	SIT_CBRow row;
	if (i < 0) i = cb->sc_SelIndex;
	if (i >= cb->sc_Items) i = cb->sc_Items - 1;
	if (i < 0) return NULL;
	row = cb->sc_RowTags + i;
	if (msg) *msg = row->entry;
	return row->tag;
}

/*
 * Owner drawn sucks:
 * - Has to be specified at creation time. Cannot use SetWindowLong.
 * - We get WM_MEASUREITEM way too early
 */
BOOL SIT_RecreateComboBox(SIT_Widget w, int style)
{
	int id;
	if (w->sw_Handle)
	{
		if (GetWindowLong(w->sw_Handle, GWL_STYLE) == style)
			return TRUE;

		id = GetWindowLong(w->sw_Handle, GWL_ID);
		DestroyWindow(w->sw_Handle);
	}
	else id = MAKE_ID(w);

	HWND hwnd = w->sw_Handle = CreateWindowEx(0, WC_COMBOBOX, w->sw_Name, style |
		(cb->sc_ReadOnly ? CBS_DROPDOWNLIST : CBS_DROPDOWN),
		0, 0, 10, 10, SIT_GetContainer(w), (HMENU) id, instance, NULL);

	if (hwnd == NULL) return FALSE;
	SetProp(hwnd, sit_class, (HANDLE) w);
	SIT_SetDefaultFont(w);

	/* Keep track of class inside callback */
	SetWindowLong(hwnd, GWL_USERDATA, (LONG) w);

	if (! cb->sc_ReadOnly && cb->sc_AutoComplete)
	{
		cb->sc_Edit = (HWND) SendMessage(hwnd, CBEM_GETEDITCONTROL, 0, 0);
		if (cb->sc_Edit == NULL) // Regular combobox
			cb->sc_Edit = FindWindowEx(hwnd, NULL, WC_EDIT, NULL);
		cb->sc_EditProc = (WNDPROC) GetWindowLong(cb->sc_Edit, GWL_WNDPROC);
		SetWindowLong(cb->sc_Edit, GWL_WNDPROC, (LONG) SIT_ComboEditProc);
		SetProp(cb->sc_Edit, sit_class, (HANDLE) w);
	}
	if (w->sw_PostProcess == NULL && cb->sc_Items > 0)
		SIT_SetComboListWithTags(cb);
	return TRUE;
}

DLLIMP int SIT_ComboDeleteItem(SIT_Widget w, int index)
{
	SIT_CBRow row;
	STRPTR    msg, end;
	if (index < 0 || index >= cb->sc_Items) index = cb->sc_Items - 1;
	if (index < 0) return -1;
	row = cb->sc_RowTags + index;
	msg = row->entry;
	end = strchr(msg, 0) + 1;
	if (index + 1 < cb->sc_Items)
		memmove(row, row + 1, cb->sc_Items - index - 1);
	if (end < cb->sc_Values + cb->sc_Count)
		memmove(msg, end, cb->sc_Count - (end - cb->sc_Values));
	if (cb->sc_SelIndex >= index)
		cb->sc_SelIndex --;
	cb->sc_Items --;
	cb->sc_Count -= end - msg;
	SendMessage(w->sw_Handle, CB_DELETESTRING, index, 0);
	return index;
}

DLLIMP int SIT_ComboInsertItem(SIT_Widget w, int index, STRPTR item, APTR rowtag)
{
	SIT_CBRow row;
	if (w == NULL) return -1;
	int max = (cb->sc_Count + 255) & ~255;
	int tag = (cb->sc_Items + 31) & ~31;
	int len = strlen(item) + 1;

	if (index < 0) index = cb->sc_Items;
	if (index <= cb->sc_SelIndex)
		cb->sc_SelIndex ++;

	/*
	 * Yep, that's right folks. Microsoft has done such as piss poor job at managing memory
	 * within listbox, that we have to handle allocation ourselves. And by piss poor, I mean
	 * 1 or 2 sec to populate a combobox (using CB_INSERTSTRING) of about 100 entries on a
	 * Core Duo 2GHz. WTF?
	 */
	if (cb->sc_Count + len > max)
	{
		STRPTR val;
		max = (cb->sc_Count + len + 255) & ~255;
		val = realloc(cb->sc_Values, max);
		if (val == NULL) return -1;
		if (cb->sc_Count == 0) val[0] = 0;
		for (max = 0, row = cb->sc_RowTags; max < cb->sc_Items; max ++, row ++)
			row->entry = val + (row->entry - cb->sc_Values);
		cb->sc_Values = val;
	}
	if (cb->sc_Items + 1 > tag)
	{
		max = (cb->sc_Items + 32) & ~31;
		cb->sc_RowTags = realloc(cb->sc_RowTags, max * sizeof *cb->sc_RowTags);
	}
	if (index < cb->sc_Items)
		memmove(cb->sc_RowTags + 1, cb->sc_RowTags, (cb->sc_Items - index) * sizeof *cb->sc_RowTags);
	row = cb->sc_RowTags + cb->sc_Items;
	row->entry = strcpy(cb->sc_Values + cb->sc_Count, item);
	row->tag   = rowtag;
	cb->sc_Items ++;
	cb->sc_Count += len;

	if (! (w->sw_Flags & SITF_Internal))
	{
		PostMessage(GetParent(w->sw_Handle), WM_COMBOCHG, 0, (LPARAM) w->sw_Handle);
		w->sw_Flags |= SITF_Internal; // Avoid registering event again
	}

	return cb->sc_Items - 1;
}
#undef cb

BOOL SIT_CreateComboBox(SIT_Widget w, va_list args)
{
	SIT_ComboBox cb = (SIT_ComboBox) w;
	int          style = WS_CHILD | CBS_AUTOHSCROLL | WS_VSCROLL;

	/* Default values */
	cb->sc_SelIndex    = -1;
	w->sw_OptimalWidth = SIT_MeasureComboBox;
	w->sw_SetValue     = SIT_SetComboValues;
	w->sw_Finalize     = SIT_FinalizeCombo;

	SIT_ParseTags(w, args, w->sw_Class = ComboClass);

	if (HAS_TAB(w))   style |= WS_TABSTOP;
	if (IsVisible(w)) style |= WS_VISIBLE;

	if (! SIT_RecreateComboBox(w, style))
		return FALSE;

	if (w->sw_PostProcess)
		w->sw_PostProcess(w, NULL, NULL);

	return TRUE;
}


